/**
 * Copyright 2006 gworks.com.au
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. (http://www.apache.org/licenses/LICENSE-2.0)
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for 
 * the specific language governing permissions and limitations under the License.
 *
 * <dhamma-message src="Atisha (11th century Tibetan Buddhist master)">
 *  The greatest achievement is selflessness. The greatest worth is self-mastery.
 *  The greatest quality is seeking to serve others. The greatest precept is continual awareness.
 *  The greatest medicine is the emptiness of everything. The greatest action is not conforming with the worlds ways.
 *  The greatest magic is transmuting the passions. The greatest generosity is non-attachment.
 *  The greatest goodness is a peaceful mind. The greatest patience is humility.
 *  The greatest effort is not concerned with results. The greatest meditation is a mind that lets go.
 *  The greatest wisdom is seeing through appearances. 
 * </dhamma-message>
 * 
 * @author Ashin Wimalajeewa (ash)
 */
package org.javaongems.user.rebind.jrpc;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

import org.javaongems.user.jclient.impl.HttpRequestUtils;
import org.javaongems.user.jrpc.impl.ClientSerializationStreamReader;
import org.javaongems.user.jrpc.impl.ClientSerializationStreamWriter;

import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.client.rpc.ServiceDefTarget;
import com.google.gwt.user.client.rpc.impl.AbstractSerializationStreamWriter;
import com.google.gwt.user.server.rpc.impl.ServerSerializableTypeOracle;
import com.google.gwt.user.server.rpc.impl.ServerSerializableTypeOracleImpl;

public class JRpcProxy implements ServiceDefTarget, InvocationHandler {
	private String url;
	private Class classLiteral;
	private ServerSerializableTypeOracle serializableTypeOracle;

	public static Object newInstance(Class classLiteral) {
		Class[] proxyInterfaces = new Class[] { classLiteral, ServiceDefTarget.class };
		return Proxy.newProxyInstance(classLiteral.getClassLoader(), proxyInterfaces, new JRpcProxy(classLiteral));
	}

	private JRpcProxy(Class classLiteral) {
		this.classLiteral = classLiteral;
		serializableTypeOracle = new ServerSerializableTypeOracleImpl(getPackagePaths());
	}

	// ServiceDefTarget
	public String getServiceEntryPoint() {
		return url;
	}

	public void setServiceEntryPoint(String address) {
		url = address;
	}

	// InvocationHandler
	public Object invoke(Object proxy, Method method, Object[] args)
			throws Throwable {
		if (ServiceDefTarget.class == method.getDeclaringClass()) 
			return invokeServiceDefTargetMethod(method, args);

		if (getServiceEntryPoint() == null)
			throw new ServiceDefTarget.NoServiceEntryPointSpecifiedException();
		String postData = createRequest(method, args);
		String response = invokeRpc(postData);
		Object result = readResponse(method, response);
		return result;
	}
	
	private Object readResponse(Method method, String encodedResponse) throws Throwable {
		ClientSerializationStreamReader streamReader = new ClientSerializationStreamReader(serializableTypeOracle);
        Object result = null;
        Throwable caught = null;
        try {
            if (encodedResponse.startsWith("{OK}")) {
              streamReader.prepareToRead(encodedResponse.substring(4));
              result = readResultFromStream(streamReader, method);
            } else if (encodedResponse.startsWith("{EX}")) {
              streamReader.prepareToRead(encodedResponse.substring(4));
              caught = (Throwable) streamReader.readObject();
            } else {
              caught = new com.google.gwt.user.client.rpc.InvocationException(encodedResponse);
            }
          } catch (Throwable e) {
            caught = e;
          }
          if (caught != null)
        	  throw caught;
          return result;
	}
	
	private Object readResultFromStream(ClientSerializationStreamReader streamReader, Method method) 
			throws SerializationException {
		Object ret = null;
		Class retType = method.getReturnType();
		if (void.class != retType) {
			if (retType.isPrimitive()) {
				if (retType == boolean.class)
					ret = new Boolean(streamReader.readBoolean());
				else if (retType == byte.class)
					ret = new Byte(streamReader.readByte());
				else if (retType == char.class)
					ret = new Character(streamReader.readChar());
				else if (retType == double.class)
					ret = new Double(streamReader.readDouble());
				else if (retType == float.class)
					ret = new Float(streamReader.readFloat());
				else if (retType == int.class)
					ret = new Integer(streamReader.readInt());
				else if (retType == long.class)
					ret = new Long(streamReader.readLong());
				else if (retType == short.class)
					ret = new Short(streamReader.readShort());
			} else {
				if (retType == String.class)
					ret = streamReader.readString();
				else
					ret = streamReader.readObject();
			}
		}
		return ret;
	}
	
	private String invokeRpc(String postData) throws Throwable{
		String ret = HttpRequestUtils.syncPost(url, postData);
		return ret;
	}

	private String createRequest(Method method, Object[] args)
			throws SerializationException {
		ClientSerializationStreamWriter streamWriter = new ClientSerializationStreamWriter(serializableTypeOracle);
		streamWriter.prepareToWrite();
		streamWriter.writeString(classLiteral.getName());
		streamWriter.writeString(method.getName());
		if (args != null) {
			streamWriter.writeInt(args.length);
			if (args.length > 0) {
				Class[] paramTypes = method.getParameterTypes();
				createRequestParameter(streamWriter, paramTypes, args);
			}
		} else
			streamWriter.writeInt(0);
		String ret = streamWriter.toString();
		return ret;
	}

	private void createRequestParameter(AbstractSerializationStreamWriter streamWriter, Class[] paramTypes, Object[] args) 
			throws SerializationException {
		for (Class paramType : paramTypes)
			streamWriter.writeString(serializableTypeOracle.getSerializedTypeName(paramType));
		for (int i = 0; i < paramTypes.length; i++) {
			Class paramType = paramTypes[i];
			if (paramType.isPrimitive()) {
				if (paramType == boolean.class)
					streamWriter.writeBoolean((Boolean) args[i]);
				else if (paramType == byte.class)
					streamWriter.writeByte((Byte) args[i]);
				else if (paramType == char.class)
					streamWriter.writeChar((Character) args[i]);
				else if (paramType == double.class)
					streamWriter.writeDouble((Double) args[i]);
				else if (paramType == float.class)
					streamWriter.writeFloat((Float) args[i]);
				else if (paramType == int.class)
					streamWriter.writeInt((Integer) args[i]);
				else if (paramType == long.class)
					streamWriter.writeLong((Long) args[i]);
				else if (paramType == short.class)
					streamWriter.writeShort((Short) args[i]);
			} else {
				if (paramType == String.class)
					streamWriter.writeString((String) args[i]);
				else
					streamWriter.writeObject(args[i]);
			}
		}
	}

	private Object invokeServiceDefTargetMethod(Method method, Object[] args)
			throws Throwable {
		String name = method.getName();
		Object ret = null;
		if (name.equals("getServiceEntryPoint"))
			ret = getServiceEntryPoint();
		else
			setServiceEntryPoint((String) args[0]);
		return ret;
	}

	/**
	 * TODO: submit a request to make
	 * gwt::RemoteServiceServlet#getPackagePaths() public and static
	 */
	private String[] getPackagePaths() {
		return new String[] { "com.google.gwt.user.client.rpc.core" };
	}
}
